Merge branch 'master' of github.com:cantino/huginn

Andrew Cantino 10 years ago
parent
commit
6e386c2ba2

+ 27 - 14
app/concerns/twitter_concern.rb

@@ -2,28 +2,41 @@ module TwitterConcern
2 2
   extend ActiveSupport::Concern
3 3
 
4 4
   included do
5
-    self.validate :validate_twitter_options
6
-    self.after_initialize :configure_twitter
5
+    validate :validate_twitter_options
6
+    after_initialize :configure_twitter
7 7
   end
8 8
 
9 9
   def validate_twitter_options
10
-    unless options['consumer_key'].present? &&
11
-      options['consumer_secret'].present? &&
12
-      options['oauth_token'].present? &&
13
-      options['oauth_token_secret'].present?
14
-      errors.add(:base, "consumer_key, consumer_secret, oauth_token and oauth_token_secret are required to authenticate with the Twitter API")
10
+    unless twitter_consumer_key.present? &&
11
+      twitter_consumer_secret.present? &&
12
+      twitter_oauth_token.present? &&
13
+      twitter_oauth_token_secret.present?
14
+      errors.add(:base, "Twitter consumer_key, consumer_secret, oauth_token, and oauth_token_secret are required to authenticate with the Twitter API.  You can provide these as options to this Agent, or as Credentials with the same names, but starting with 'twitter_'.")
15 15
     end
16 16
   end
17 17
 
18
+  def twitter_consumer_key
19
+    options['consumer_key'].presence || credential('twitter_consumer_key')
20
+  end
21
+
22
+  def twitter_consumer_secret
23
+    options['consumer_secret'].presence || credential('twitter_consumer_secret')
24
+  end
25
+
26
+  def twitter_oauth_token
27
+    options['oauth_token'].presence || options['access_key'].presence || credential('twitter_oauth_token')
28
+  end
29
+
30
+  def twitter_oauth_token_secret
31
+    options['oauth_token_secret'].presence || options['access_secret'].presence || credential('twitter_oauth_token_secret')
32
+  end
33
+
18 34
   def configure_twitter
19 35
     Twitter.configure do |config|
20
-      config.consumer_key = options['consumer_key']
21
-      config.consumer_secret = options['consumer_secret']
22
-      config.oauth_token = options['oauth_token'] || options['access_key']
23
-      config.oauth_token_secret = options['oauth_token_secret'] || options['access_secret']
36
+      config.consumer_key = twitter_consumer_key
37
+      config.consumer_secret = twitter_consumer_secret
38
+      config.oauth_token = twitter_oauth_token
39
+      config.oauth_token_secret = twitter_oauth_token_secret
24 40
     end
25 41
   end
26
-
27
-  module ClassMethods
28
-  end
29 42
 end

+ 61 - 0
app/controllers/user_credentials_controller.rb

@@ -0,0 +1,61 @@
1
+class UserCredentialsController < ApplicationController
2
+  def index
3
+    @user_credentials = current_user.user_credentials.page(params[:page])
4
+
5
+    respond_to do |format|
6
+      format.html
7
+      format.json { render json: @user_credentials }
8
+    end
9
+  end
10
+
11
+  def new
12
+    @user_credential = current_user.user_credentials.build
13
+
14
+    respond_to do |format|
15
+      format.html
16
+      format.json { render json: @user_credential }
17
+    end
18
+  end
19
+
20
+  def edit
21
+    @user_credential = current_user.user_credentials.find(params[:id])
22
+  end
23
+
24
+  def create
25
+    @user_credential = current_user.user_credentials.build(params[:user_credential])
26
+
27
+    respond_to do |format|
28
+      if @user_credential.save
29
+        format.html { redirect_to user_credentials_path, notice: 'Your credential was successfully created.' }
30
+        format.json { render json: @user_credential, status: :created, location: @user_credential }
31
+      else
32
+        format.html { render action: "new" }
33
+        format.json { render json: @user_credential.errors, status: :unprocessable_entity }
34
+      end
35
+    end
36
+  end
37
+
38
+  def update
39
+    @user_credential = current_user.user_credentials.find(params[:id])
40
+
41
+    respond_to do |format|
42
+      if @user_credential.update_attributes(params[:user_credential])
43
+        format.html { redirect_to user_credentials_path, notice: 'Your credential was successfully updated.' }
44
+        format.json { head :no_content }
45
+      else
46
+        format.html { render action: "edit" }
47
+        format.json { render json: @user_credential.errors, status: :unprocessable_entity }
48
+      end
49
+    end
50
+  end
51
+
52
+  def destroy
53
+    @user_credential = current_user.user_credentials.find(params[:id])
54
+    @user_credential.destroy
55
+
56
+    respond_to do |format|
57
+      format.html { redirect_to user_credentials_path }
58
+      format.json { head :no_content }
59
+    end
60
+  end
61
+end

+ 14 - 0
app/models/agent.rb

@@ -102,6 +102,20 @@ class Agent < ActiveRecord::Base
102 102
     end
103 103
   end
104 104
 
105
+  def credential(name)
106
+    @credential_cache ||= {}
107
+    if @credential_cache.has_key?(name)
108
+      @credential_cache[name]
109
+    else
110
+      @credential_cache[name] = user.user_credentials.where(:credential_name => name).first.try(:credential_value)
111
+    end
112
+  end
113
+
114
+  def reload
115
+    @credential_cache = {}
116
+    super
117
+  end
118
+
105 119
   def new_event_expiration_date
106 120
     keep_events_for > 0 ? keep_events_for.days.from_now : nil
107 121
   end

+ 6 - 14
app/models/agents/twitter_publish_agent.rb

@@ -8,10 +8,11 @@ module Agents
8 8
     description <<-MD
9 9
       The TwitterPublishAgent publishes tweets from the events it receives.
10 10
 
11
-      You [must set up a Twitter app](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token) and provide it's `consumer_key`, `consumer_secret`, `oauth_token` and `oauth_token_secret`,
12
-      (also knows as "Access token" on the Twitter developer's site), along with the `username` of the Twitter user to publish as.
11
+      Twitter credentials must be supplied as either [credentials](/user_credentials) called
12
+      `twitter_consumer_key`, `twitter_consumer_secret`, `twitter_oauth_token`, and `twitter_oauth_token_secret`,
13
+      or as options to this Agent called `consumer_key`, `consumer_secret`, `oauth_token`, and `oauth_token_secret`.
13 14
 
14
-      The `oauth_token` and `oauth_token_secret` determine which user the tweet will be sent as.
15
+      To get oAuth credentials for Twitter, [follow these instructions](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token).
15 16
 
16 17
       You must also specify a `message_path` parameter: a [JSONPaths](http://goessner.net/articles/JsonPath/) to the value to tweet.
17 18
 
@@ -19,10 +20,7 @@ module Agents
19 20
     MD
20 21
 
21 22
     def validate_options
22
-      unless options['username'].present? &&
23
-        options['expected_update_period_in_days'].present?
24
-        errors.add(:base, "username and expected_update_period_in_days are required")
25
-      end      
23
+      errors.add(:base, "expected_update_period_in_days is required") unless options['expected_update_period_in_days'].present?
26 24
     end
27 25
 
28 26
     def working?
@@ -31,12 +29,7 @@ module Agents
31 29
 
32 30
     def default_options
33 31
       {
34
-        'username' => "",
35 32
         'expected_update_period_in_days' => "10",
36
-        'consumer_key' => "---",
37
-        'consumer_secret' => "---",
38
-        'oauth_token' => "---",
39
-        'oauth_token_secret' => "---",
40 33
         'message_path' => "text"
41 34
       }
42 35
     end
@@ -68,9 +61,8 @@ module Agents
68 61
       end
69 62
     end
70 63
 
71
-    def publish_tweet text
64
+    def publish_tweet(text)
72 65
       Twitter.update(text)
73 66
     end
74
-
75 67
   end
76 68
 end

+ 8 - 10
app/models/agents/twitter_stream_agent.rb

@@ -6,11 +6,13 @@ module Agents
6 6
     description <<-MD
7 7
       The TwitterStreamAgent follows the Twitter stream in real time, watching for certain keywords, or filters, that you provide.
8 8
 
9
-      You must provide an oAuth `consumer_key`, `consumer_secret`, `oauth_token`, and `oauth_token_secret`, as well as an array of `filters`.  Multiple words in a filter
10
-      must all show up in a tweet, but are independent of order.
11
-
9
+      To follow the Twitter stream, provide an array of `filters`.  Multiple words in a filter must all show up in a tweet, but are independent of order.
12 10
       If you provide an array instead of a filter, the first entry will be considered primary and any additional values will be treated as aliases.
13 11
 
12
+      Twitter credentials must be supplied as either [credentials](/user_credentials) called
13
+      `twitter_consumer_key`, `twitter_consumer_secret`, `twitter_oauth_token`, and `twitter_oauth_token_secret`,
14
+      or as options to this Agent called `consumer_key`, `consumer_secret`, `oauth_token`, and `oauth_token_secret`.
15
+
14 16
       To get oAuth credentials for Twitter, [follow these instructions](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token).
15 17
 
16 18
       Set `expected_update_period_in_days` to the maximum amount of time that you'd expect to pass between Events being created by this Agent.
@@ -67,13 +69,9 @@ module Agents
67 69
 
68 70
     def default_options
69 71
       {
70
-          'consumer_key' => "---",
71
-          'consumer_secret' => "---",
72
-          'oauth_token' => "---",
73
-          'oauth_token_secret' => "---",
74
-          'filters' => %w[keyword1 keyword2],
75
-          'expected_update_period_in_days' => "2",
76
-          'generate' => "events"
72
+        'filters' => %w[keyword1 keyword2],
73
+        'expected_update_period_in_days' => "2",
74
+        'generate' => "events"
77 75
       }
78 76
     end
79 77
 

+ 9 - 7
app/models/agents/twitter_user_agent.rb

@@ -9,7 +9,13 @@ module Agents
9 9
     description <<-MD
10 10
       The TwitterUserAgent follows the timeline of a specified Twitter user.
11 11
 
12
-      You [must set up a Twitter app](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token) and provide it's `consumer_key`, `consumer_secret`, `oauth_token` and `oauth_token_secret`, (Also shown as "Access token" on the Twitter developer's site.) along with the `username` of the Twitter user to monitor.
12
+      Twitter credentials must be supplied as either [credentials](/user_credentials) called
13
+      `twitter_consumer_key`, `twitter_consumer_secret`, `twitter_oauth_token`, and `twitter_oauth_token_secret`,
14
+      or as options to this Agent called `consumer_key`, `consumer_secret`, `oauth_token`, and `oauth_token_secret`.
15
+
16
+      To get oAuth credentials for Twitter, [follow these instructions](https://github.com/cantino/huginn/wiki/Getting-a-twitter-oauth-token).
17
+
18
+      You must also provide the `username` of the Twitter user to monitor.
13 19
 
14 20
       Set `expected_update_period_in_days` to the maximum amount of time that you'd expect to pass between Events being created by this Agent.
15 21
     MD
@@ -53,12 +59,8 @@ module Agents
53 59
 
54 60
     def default_options
55 61
       {
56
-          'username' => "tectonic",
57
-          'expected_update_period_in_days' => "2",
58
-          'consumer_key' => "---",
59
-          'consumer_secret' => "---",
60
-          'oauth_token' => "---",
61
-          'oauth_token_secret' => "---"
62
+        'username' => "tectonic",
63
+        'expected_update_period_in_days' => "2"
62 64
       }
63 65
     end
64 66
 

+ 1 - 0
app/models/contact.rb

@@ -1,5 +1,6 @@
1 1
 # Contacts are used only for the contact form on the Huginn website.  If you host a public Huginn instance, you can use
2 2
 # these to receive messages from visitors.
3
+
3 4
 class Contact < ActiveRecord::Base
4 5
   attr_accessible :email, :message, :name
5 6
 

+ 1 - 0
app/models/user.rb

@@ -22,6 +22,7 @@ class User < ActiveRecord::Base
22 22
   validates_format_of :username, :with => /\A[a-zA-Z0-9_-]{3,15}\Z/, :message => "can only contain letters, numbers, underscores, and dashes, and must be between 3 and 15 characters in length."
23 23
   validates_inclusion_of :invitation_code, :on => :create, :in => INVITATION_CODES, :message => "is not valid"
24 24
 
25
+  has_many :user_credentials, :dependent => :destroy, :inverse_of => :user
25 26
   has_many :events, :order => "events.created_at desc", :dependent => :delete_all, :inverse_of => :user
26 27
   has_many :agents, :order => "agents.created_at desc", :dependent => :destroy, :inverse_of => :user
27 28
   has_many :logs, :through => :agents, :class_name => "AgentLog"

+ 19 - 0
app/models/user_credential.rb

@@ -0,0 +1,19 @@
1
+class UserCredential < ActiveRecord::Base
2
+  attr_accessible :credential_name, :credential_value
3
+
4
+  belongs_to :user
5
+
6
+  validates_presence_of :credential_name
7
+  validates_presence_of :credential_value
8
+  validates_presence_of :user_id
9
+  validates_uniqueness_of :credential_name, :scope => :user_id
10
+
11
+  before_save :trim_fields
12
+
13
+  protected
14
+
15
+  def trim_fields
16
+    credential_name.strip!
17
+    credential_value.strip!
18
+  end
19
+end

+ 1 - 1
app/views/devise/registrations/edit.html.erb

@@ -58,4 +58,4 @@
58 58
       </div>
59 59
     </div>
60 60
   </div>
61
-</div>
61
+</div>

+ 1 - 0
app/views/layouts/_navigation.html.erb

@@ -4,6 +4,7 @@
4 4
   <ul class='nav pull-left'>
5 5
     <%= nav_link "Agents", agents_path %>
6 6
     <%= nav_link "Events", events_path %>
7
+    <%= nav_link "Credentials", user_credentials_path %>
7 8
   </ul>
8 9
 <% end %>
9 10
 

+ 30 - 0
app/views/user_credentials/_form.html.erb

@@ -0,0 +1,30 @@
1
+<%= form_for(@user_credential, :method => @user_credential.new_record? ? "POST" : "PUT") do |f| %>
2
+  <% if @user_credential.errors.any? %>
3
+    <div id="error_explanation">
4
+      <h2><%= pluralize(@user_credential.errors.count, "error") %> prohibited this Credential from being saved:</h2>
5
+      <ul>
6
+      <% @user_credential.errors.full_messages.each do |msg| %>
7
+        <li><%= msg %></li>
8
+      <% end %>
9
+      </ul>
10
+    </div>
11
+  <% end %>
12
+
13
+  <div class="control-group">
14
+    <%= f.label :credential_name, :class => 'control-label' %>
15
+    <div class="controls">
16
+      <%= f.text_field :credential_name, :class => 'span4' %>
17
+    </div>
18
+  </div>
19
+
20
+  <div class="control-group">
21
+    <%= f.label :credential_value, :class => 'control-label' %>
22
+    <div class="controls">
23
+      <%= f.text_area :credential_value, :class => 'span8', :rows => 10 %>
24
+    </div>
25
+  </div>
26
+
27
+  <div class='form-actions' style='clear: both'>
28
+    <%= f.submit "Save Credential", :class => "btn btn-primary" %>
29
+  </div>
30
+<% end %>

+ 17 - 0
app/views/user_credentials/edit.html.erb

@@ -0,0 +1,17 @@
1
+<div class='container'>
2
+  <div class='row'>
3
+    <div class='span12'>
4
+      <div class="page-header">
5
+        <h2>
6
+          Editing your Credential
7
+        </h2>
8
+      </div>
9
+
10
+      <%= render 'form' %>
11
+
12
+      <div class="btn-group">
13
+        <%= link_to '<i class="icon-chevron-left"></i> Back'.html_safe, user_credentials_path, class: "btn" %>
14
+      </div>
15
+    </div>
16
+  </div>
17
+</div>

+ 44 - 0
app/views/user_credentials/index.html.erb

@@ -0,0 +1,44 @@
1
+<div class='container'>
2
+  <div class='row'>
3
+    <div class='span12'>
4
+      <div class="page-header">
5
+        <h2>
6
+          Your Credentials
7
+        </h2>
8
+      </div>
9
+
10
+      <blockquote>
11
+        Credentials are used to store values used by many Agents. Examples might include "twitter_consumer_secret",
12
+        "user_full_name", or "user_birthday".
13
+      </blockquote>
14
+
15
+      <table class='table table-striped'>
16
+        <tr>
17
+          <th>Name</th>
18
+          <th>Value</th>
19
+        </tr>
20
+
21
+        <% @user_credentials.each do |user_credential| %>
22
+          <tr>
23
+            <td><%= user_credential.credential_name %></td>
24
+            <td>
25
+              <%= truncate user_credential.credential_value %>
26
+              <div class="btn-group" style="float: right">
27
+                <%= link_to 'Edit', edit_user_credential_path(user_credential), class: "btn btn-mini" %>
28
+                <%= link_to 'Delete', user_credential_path(user_credential), method: :delete, data: {confirm: 'Are you sure?'}, class: "btn btn-mini" %>
29
+              </div>
30
+            </td>
31
+          </tr>
32
+        <% end %>
33
+      </table>
34
+
35
+      <%= paginate @user_credentials, :theme => 'twitter-bootstrap' %>
36
+
37
+      <br/>
38
+
39
+      <div class="btn-group">
40
+        <%= link_to '<i class="icon-plus"></i> New Credential'.html_safe, new_user_credential_path, class: "btn" %>
41
+      </div>
42
+    </div>
43
+  </div>
44
+</div>

+ 17 - 0
app/views/user_credentials/new.html.erb

@@ -0,0 +1,17 @@
1
+<div class='container'>
2
+  <div class='row'>
3
+    <div class='span12'>
4
+      <div class="page-header">
5
+        <h2>
6
+          Create a new Credential
7
+        </h2>
8
+      </div>
9
+
10
+      <%= render 'form' %>
11
+
12
+      <div class="btn-group">
13
+        <%= link_to '<i class="icon-chevron-left"></i> Back'.html_safe, user_credentials_path, class: "btn" %>
14
+      </div>
15
+    </div>
16
+  </div>
17
+</div>

+ 9 - 11
bin/twitter_stream.rb

@@ -18,16 +18,16 @@ require 'twitter/json_stream'
18 18
 require 'em-http-request'
19 19
 require 'pp'
20 20
 
21
-def stream!(filters, options = {}, &block)
21
+def stream!(filters, agent, &block)
22 22
   stream = Twitter::JSONStream.connect(
23 23
     :path    => "/1/statuses/#{(filters && filters.length > 0) ? 'filter' : 'sample'}.json#{"?track=#{filters.map {|f| CGI::escape(f) }.join(",")}" if filters && filters.length > 0}",
24
+    :ssl     => true,
24 25
     :oauth   => {
25
-      :consumer_key    => options[:consumer_key],
26
-      :consumer_secret => options[:consumer_secret],
27
-      :access_key      => options[:oauth_token] || options[:access_key],
28
-      :access_secret   => options[:oauth_token_secret] || options[:access_secret]
29
-    },
30
-    :ssl     => true
26
+      :consumer_key    => agent.twitter_consumer_key,
27
+      :consumer_secret => agent.twitter_consumer_secret,
28
+      :access_key      => agent.twitter_oauth_token,
29
+      :access_secret   => agent.twitter_oauth_token_secret
30
+    }
31 31
   )
32 32
 
33 33
   stream.each_item do |status|
@@ -55,7 +55,7 @@ def stream!(filters, options = {}, &block)
55 55
 end
56 56
 
57 57
 def load_and_run(agents)
58
-  agents.group_by { |agent| agent.options[:twitter_username] }.each do |twitter_username, agents|
58
+  agents.group_by { |agent| agent.twitter_oauth_token }.each do |oauth_token, agents|
59 59
     filter_to_agent_map = agents.map { |agent| agent.options[:filters] }.flatten.uniq.compact.map(&:strip).inject({}) { |m, f| m[f] = []; m }
60 60
 
61 61
     agents.each do |agent|
@@ -64,11 +64,9 @@ def load_and_run(agents)
64 64
       end
65 65
     end
66 66
 
67
-    options = agents.first.options.slice(:consumer_key, :consumer_secret, :access_key, :oauth_token, :access_secret, :oauth_token_secret)
68
-
69 67
     recent_tweets = []
70 68
 
71
-    stream!(filter_to_agent_map.keys, options) do |status|
69
+    stream!(filter_to_agent_map.keys, agents.first) do |status|
72 70
       if status["retweeted_status"].present? && status["retweeted_status"].is_a?(Hash)
73 71
         puts "Skipping retweet: #{status["text"]}"
74 72
       elsif recent_tweets.include?(status["id_str"])

+ 2 - 0
config/routes.rb

@@ -26,6 +26,8 @@ Huginn::Application.routes.draw do
26 26
     end
27 27
   end
28 28
 
29
+  resources :user_credentials, :except => :show
30
+
29 31
   match "/worker_status" => "worker_status#show"
30 32
 
31 33
   post "/users/:user_id/update_location/:secret" => "user_location_updates#create"

+ 12 - 0
db/migrate/20140121075418_create_user_credentials.rb

@@ -0,0 +1,12 @@
1
+class CreateUserCredentials < ActiveRecord::Migration
2
+  def change
3
+    create_table :user_credentials do |t|
4
+      t.integer :user_id,         :null => false
5
+      t.string :credential_name,  :null => false
6
+      t.text :credential_value,   :null => false
7
+
8
+      t.timestamps
9
+    end
10
+    add_index :user_credentials, [:user_id, :credential_name], :unique => true
11
+  end
12
+end

+ 37 - 19
db/schema.rb

@@ -11,21 +11,21 @@
11 11
 #
12 12
 # It's strongly recommended to check this file into your version control system.
13 13
 
14
-ActiveRecord::Schema.define(:version => 20131227000021) do
14
+ActiveRecord::Schema.define(:version => 20140127164931) do
15 15
 
16 16
   create_table "agent_logs", :force => true do |t|
17
-    t.integer  "agent_id",                         :null => false
18
-    t.text     "message",                          :null => false
19
-    t.integer  "level",             :default => 3, :null => false
17
+    t.integer  "agent_id",                                             :null => false
18
+    t.text     "message",           :limit => 16777215,                :null => false
19
+    t.integer  "level",                                 :default => 3, :null => false
20 20
     t.integer  "inbound_event_id"
21 21
     t.integer  "outbound_event_id"
22
-    t.datetime "created_at",                       :null => false
23
-    t.datetime "updated_at",                       :null => false
22
+    t.datetime "created_at",                                           :null => false
23
+    t.datetime "updated_at",                                           :null => false
24 24
   end
25 25
 
26 26
   create_table "agents", :force => true do |t|
27 27
     t.integer  "user_id"
28
-    t.text     "options"
28
+    t.text     "options",               :limit => 16777215
29 29
     t.string   "type"
30 30
     t.string   "name"
31 31
     t.string   "schedule"
@@ -37,27 +37,35 @@ ActiveRecord::Schema.define(:version => 20131227000021) do
37 37
     t.datetime "updated_at",                                                 :null => false
38 38
     t.text     "memory",                :limit => 2147483647
39 39
     t.datetime "last_webhook_at"
40
-    t.integer  "keep_events_for",                             :default => 0, :null => false
41 40
     t.datetime "last_event_at"
42 41
     t.datetime "last_error_log_at"
42
+    t.integer  "keep_events_for",                             :default => 0, :null => false
43 43
   end
44 44
 
45 45
   add_index "agents", ["schedule"], :name => "index_agents_on_schedule"
46 46
   add_index "agents", ["type"], :name => "index_agents_on_type"
47 47
   add_index "agents", ["user_id", "created_at"], :name => "index_agents_on_user_id_and_created_at"
48 48
 
49
+  create_table "contacts", :force => true do |t|
50
+    t.text     "message"
51
+    t.string   "name"
52
+    t.string   "email"
53
+    t.datetime "created_at", :null => false
54
+    t.datetime "updated_at", :null => false
55
+  end
56
+
49 57
   create_table "delayed_jobs", :force => true do |t|
50
-    t.integer  "priority",   :default => 0
51
-    t.integer  "attempts",   :default => 0
52
-    t.text     "handler"
53
-    t.text     "last_error"
58
+    t.integer  "priority",                       :default => 0
59
+    t.integer  "attempts",                       :default => 0
60
+    t.text     "handler",    :limit => 16777215
61
+    t.text     "last_error", :limit => 16777215
54 62
     t.datetime "run_at"
55 63
     t.datetime "locked_at"
56 64
     t.datetime "failed_at"
57 65
     t.string   "locked_by"
58 66
     t.string   "queue"
59
-    t.datetime "created_at",                :null => false
60
-    t.datetime "updated_at",                :null => false
67
+    t.datetime "created_at",                                    :null => false
68
+    t.datetime "updated_at",                                    :null => false
61 69
   end
62 70
 
63 71
   add_index "delayed_jobs", ["priority", "run_at"], :name => "delayed_jobs_priority"
@@ -65,11 +73,11 @@ ActiveRecord::Schema.define(:version => 20131227000021) do
65 73
   create_table "events", :force => true do |t|
66 74
     t.integer  "user_id"
67 75
     t.integer  "agent_id"
68
-    t.decimal  "lat",                            :precision => 15, :scale => 10
69
-    t.decimal  "lng",                            :precision => 15, :scale => 10
70
-    t.text     "payload",    :limit => 16777215
71
-    t.datetime "created_at",                                                     :null => false
72
-    t.datetime "updated_at",                                                     :null => false
76
+    t.decimal  "lat",                              :precision => 15, :scale => 10
77
+    t.decimal  "lng",                              :precision => 15, :scale => 10
78
+    t.text     "payload",    :limit => 2147483647
79
+    t.datetime "created_at",                                                       :null => false
80
+    t.datetime "updated_at",                                                       :null => false
73 81
     t.datetime "expires_at"
74 82
   end
75 83
 
@@ -87,6 +95,16 @@ ActiveRecord::Schema.define(:version => 20131227000021) do
87 95
   add_index "links", ["receiver_id", "source_id"], :name => "index_links_on_receiver_id_and_source_id"
88 96
   add_index "links", ["source_id", "receiver_id"], :name => "index_links_on_source_id_and_receiver_id"
89 97
 
98
+  create_table "user_credentials", :force => true do |t|
99
+    t.integer  "user_id",          :null => false
100
+    t.string   "credential_name",  :null => false
101
+    t.text     "credential_value", :null => false
102
+    t.datetime "created_at",       :null => false
103
+    t.datetime "updated_at",       :null => false
104
+  end
105
+
106
+  add_index "user_credentials", ["user_id", "credential_name"], :name => "index_user_credentials_on_user_id_and_credential_name", :unique => true
107
+
90 108
   create_table "users", :force => true do |t|
91 109
     t.string   "email",                  :default => "",    :null => false
92 110
     t.string   "encrypted_password",     :default => "",    :null => false

+ 85 - 0
spec/controllers/user_credentials_controller_spec.rb

@@ -0,0 +1,85 @@
1
+require 'spec_helper'
2
+
3
+describe UserCredentialsController do
4
+  def valid_attributes(options = {})
5
+    {
6
+      :credential_name => "some_name",
7
+      :credential_value => "some_value"
8
+    }.merge(options)
9
+  end
10
+
11
+  before do
12
+    sign_in users(:bob)
13
+  end
14
+
15
+  describe "GET index" do
16
+    it "only returns UserCredentials for the current user" do
17
+      get :index
18
+      assigns(:user_credentials).all? {|i| i.user.should == users(:bob) }.should be_true
19
+    end
20
+  end
21
+
22
+  describe "GET edit" do
23
+    it "only shows UserCredentials for the current user" do
24
+      get :edit, :id => user_credentials(:bob_aws_secret).to_param
25
+      assigns(:user_credential).should eq(user_credentials(:bob_aws_secret))
26
+
27
+      lambda {
28
+        get :edit, :id => user_credentials(:jane_aws_secret).to_param
29
+      }.should raise_error(ActiveRecord::RecordNotFound)
30
+    end
31
+  end
32
+
33
+  describe "POST create" do
34
+    it "creates UserCredentials for the current user" do
35
+      expect {
36
+        post :create, :user_credential => valid_attributes
37
+      }.to change { users(:bob).user_credentials.count }.by(1)
38
+    end
39
+
40
+    it "shows errors" do
41
+      expect {
42
+        post :create, :user_credential => valid_attributes(:credential_name => "")
43
+      }.not_to change { users(:bob).user_credentials.count }
44
+      assigns(:user_credential).should have(1).errors_on(:credential_name)
45
+      response.should render_template("new")
46
+    end
47
+
48
+    it "will not create UserCredentials for other users" do
49
+      expect {
50
+        post :create, :user_credential => valid_attributes(:user_id => users(:jane).id)
51
+      }.to raise_error(ActiveModel::MassAssignmentSecurity::Error)
52
+    end
53
+  end
54
+
55
+  describe "PUT update" do
56
+    it "updates attributes on UserCredentials for the current user" do
57
+      post :update, :id => user_credentials(:bob_aws_key).to_param, :user_credential => { :credential_name => "new_name" }
58
+      response.should redirect_to(user_credentials_path)
59
+      user_credentials(:bob_aws_key).reload.credential_name.should == "new_name"
60
+
61
+      lambda {
62
+        post :update, :id => user_credentials(:jane_aws_key).to_param, :user_credential => { :credential_name => "new_name" }
63
+      }.should raise_error(ActiveRecord::RecordNotFound)
64
+      user_credentials(:jane_aws_key).reload.credential_name.should_not == "new_name"
65
+    end
66
+
67
+    it "shows errors" do
68
+      post :update, :id => user_credentials(:bob_aws_key).to_param, :user_credential => { :credential_name => "" }
69
+      assigns(:user_credential).should have(1).errors_on(:credential_name)
70
+      response.should render_template("edit")
71
+    end
72
+  end
73
+
74
+  describe "DELETE destroy" do
75
+    it "destroys only UserCredentials owned by the current user" do
76
+      expect {
77
+        delete :destroy, :id => user_credentials(:bob_aws_key).to_param
78
+      }.to change(UserCredential, :count).by(-1)
79
+
80
+      lambda {
81
+        delete :destroy, :id => user_credentials(:jane_aws_key).to_param
82
+      }.should raise_error(ActiveRecord::RecordNotFound)
83
+    end
84
+  end
85
+end

+ 16 - 0
spec/fixtures/user_credentials.yml

@@ -0,0 +1,16 @@
1
+bob_aws_key:
2
+  user: bob
3
+  credential_name: aws_key
4
+  credential_value: 2222222222-bob
5
+bob_aws_secret:
6
+  user: bob
7
+  credential_name: aws_secret
8
+  credential_value: 1111111111-bob
9
+jane_aws_key:
10
+  user: jane
11
+  credential_name: aws_key
12
+  credential_value: 2222222222-jane
13
+jane_aws_secret:
14
+  user: jane
15
+  credential_name: aws_secret
16
+  credential_value: 1111111111-jabe

+ 20 - 1
spec/models/agent_spec.rb

@@ -27,6 +27,25 @@ describe Agent do
27 27
     end
28 28
   end
29 29
 
30
+  describe "credential" do
31
+    it "should return the value of the credential when credential is present" do
32
+      agents(:bob_weather_agent).credential("aws_secret").should == user_credentials(:bob_aws_secret).credential_value
33
+    end
34
+
35
+    it "should return nil when credential is not present" do
36
+      agents(:bob_weather_agent).credential("non_existing_credential").should == nil
37
+    end
38
+
39
+    it "should memoize the load" do
40
+      mock.any_instance_of(UserCredential).credential_value.twice { "foo" }
41
+      agents(:bob_weather_agent).credential("aws_secret").should == "foo"
42
+      agents(:bob_weather_agent).credential("aws_secret").should == "foo"
43
+      agents(:bob_weather_agent).reload
44
+      agents(:bob_weather_agent).credential("aws_secret").should == "foo"
45
+      agents(:bob_weather_agent).credential("aws_secret").should == "foo"
46
+    end
47
+  end
48
+
30 49
   describe "changes to type" do
31 50
     it "validates types" do
32 51
       source = Agent.new
@@ -473,4 +492,4 @@ describe Agent do
473 492
       end
474 493
     end
475 494
   end
476
-end
495
+end

+ 29 - 0
spec/models/user_credential_spec.rb

@@ -0,0 +1,29 @@
1
+require 'spec_helper'
2
+
3
+describe UserCredential do
4
+  describe "validation" do
5
+    it { should validate_uniqueness_of(:credential_name).scoped_to(:user_id) }
6
+    it { should validate_presence_of(:credential_name) }
7
+    it { should validate_presence_of(:credential_value) }
8
+    it { should validate_presence_of(:user_id) }
9
+  end
10
+
11
+  describe "mass assignment" do
12
+    it { should allow_mass_assignment_of :credential_name }
13
+
14
+    it { should allow_mass_assignment_of :credential_value }
15
+
16
+    it { should_not allow_mass_assignment_of :user_id }
17
+  end
18
+
19
+  describe "cleaning fields" do
20
+    it "should trim whitespace" do
21
+      user_credential = user_credentials(:bob_aws_key)
22
+      user_credential.credential_name = " new name "
23
+      user_credential.credential_value = " new value "
24
+      user_credential.save!
25
+      user_credential.credential_name.should == "new name"
26
+      user_credential.credential_value.should == "new value"
27
+    end
28
+  end
29
+end

+ 7 - 7
spec/models/users_spec.rb

@@ -4,16 +4,16 @@ describe User do
4 4
   describe "validations" do
5 5
     describe "invitation_code" do
6 6
       it "only accepts valid invitation codes" do
7
-      	User::INVITATION_CODES.each do |v|
8
-    			should allow_value(v).for(:invitation_code)
9
-  			end
7
+        User::INVITATION_CODES.each do |v|
8
+          should allow_value(v).for(:invitation_code)
9
+        end
10 10
       end
11 11
 
12 12
       it "can reject invalid invitation codes" do
13
-      	%w['foo', 'bar'].each do |v|
14
-    			should_not allow_value(v).for(:invitation_code)
15
-  			end
13
+        %w['foo', 'bar'].each do |v|
14
+          should_not allow_value(v).for(:invitation_code)
15
+        end
16 16
       end
17 17
     end
18 18
   end
19
-end
19
+end